﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CodeSandbox.Configuration;
using CodeSandbox.Contracts;
using CodeSandbox.Core;

namespace CodeSandbox.Controllers
{
    internal class MappingEngine : IMapper
    {
        static MappingEngine()
        {
            Instance = new MappingEngine();
        }

        public static IMapper Instance { get; private set; }

        public TDestination Map<TSource, TDestination>(TSource source, TDestination destination,
            object parentObject = null, params Expression<Func<object>>[] parameters) where TSource : class
            where TDestination : class, new()
        {
            if (source == null || destination == null) return null;
            var sourceType = source.GetType();
            if (parameters != null)
                foreach (var expression in parameters)
                {
                    var key = string.Format("{0}.{1}", typeof(MapperBase<TSource, TDestination>),
                        ((MemberExpression) ((UnaryExpression) expression.Body).Operand).Member.Name);
                    Sandbox.Cache.AddOrUpdate(key, expression, (k, e) => expression);
                }

            if (parentObject != null)
                Sandbox.Cache.TryAdd(string.Format("{0}.ParentMapper", typeof(MapperBase<TSource, TDestination>)),
                    parentObject);

            foreach (var property in destination.GetType().GetProperties())
            {
                try
                {
                    if (sourceType == typeof(DataRow))
                    {
                        DataRow data = source as DataRow;
                        if (data != null && data.Table.Columns.Contains(property.Name))
                            property.SetValue(destination,
                                data[property.Name] != null ? data[property.Name].MapTo(property.PropertyType) : null);
                    }
                    else
                    {
                        var sourceP = sourceType.GetProperty(property.Name);
                        if (sourceP != null && property.PropertyType == sourceP.PropertyType)
                        {
                            property.SetValue(destination, sourceP.GetValue(source));
                        }
                    }
                }
                catch (Exception)
                {
                    //throw;
                }

            }
            
                var mappings = (IEnumerable<Action<TSource, TDestination>>)Sandbox.Cache.GetOrAdd(typeof(MapperBase<TSource, TDestination>).ToString(),
                        key =>
                        {
                            var configMaps = CollectConfigMappings<TSource, TDestination>(parentObject);
                            var staticMapper = DependencyLibrary.GetMapping<TSource, TDestination>();
                            if (staticMapper != null)
                            {
                                ((MapperBase<TSource,TDestination>)staticMapper).AdditionalParameters = parameters;
                                ((MapperBase<TSource, TDestination>)staticMapper).DestinationObject = destination;
                                ((MapperBase<TSource, TDestination>)staticMapper).ParentObject = parentObject;
                                configMaps = configMaps.Union(((MapperBase<TSource, TDestination>)staticMapper).RegisterMapRules());
                                //if (!configMaps.Any()) throw new MissingFieldException(string.Format("Mapping information for {0} not available", typeof(MapperBase<TSource, TDestination>).ToString()));
                            }
                            return configMaps;
                        });
                object removed;
                if (!mappings.Any())
                    Sandbox.Cache.TryRemove(typeof(MapperBase<TSource, TDestination>).ToString(), out removed);
                foreach (Action<TSource, TDestination> mapping in mappings)
                {
                    mapping(source, destination);
                }
            
            return destination;
        }
        public TDestination Map<TSource, TDestination>(TSource source, object parentObject = null, params Expression<Func<object>>[] parameters)
            where TSource : class
            where TDestination : class,new()
        {
            var destination = Activator.CreateInstance<TDestination>();
            return Map(source, destination,parentObject,parameters);
        }
        
        private static IEnumerable<Action<TSource, TDestination>> CollectConfigMappings<TSource, TDestination>(object parentObject) where TDestination : class, new()
        {
            var mappings = new List<Action<TSource, TDestination>>();
            var key = string.Format("{0}To{1}", typeof(TSource).FullName, typeof(TDestination).FullName);
            var maps = ConfigurationManager.GetSection("mappingConfiguration") as MappingConfigurationSection;
            if (maps == null ||
                !maps.Mappings.OfType<MappingElement>()
                    .Any(m => m.Source == typeof(TSource).FullName && m.Destination == typeof(TDestination).FullName))
                return mappings;
            return (IEnumerable<Action<TSource, TDestination>>)Sandbox.Cache.GetOrAdd(key, k =>
            {
                foreach (MappingElement mapping in maps.Mappings)
                {
                    if (mapping.Source != typeof(TSource).FullName || mapping.Destination != typeof(TDestination).FullName) continue;
                    foreach (var map in mapping.Maps.OfType<MapElement>().ToArray())
                    {
                        var sourceParam = Expression.Parameter(typeof(TSource), "source");
                        var destinationParam = Expression.Parameter(typeof(TDestination), "destination");
                        var parentObjectParam = Expression.Parameter(parentObject!=null? parentObject.GetType(): typeof(object), "ParentObject");
                        var e = DynamicCodeExpression.ParseLambda(new[] { sourceParam, destinationParam, parentObjectParam }, null, map.SetExpression);
                        mappings.Add((source, destination) =>
                        {
                            var value = e.Compile().DynamicInvoke(source, destination,parentObject);
                            if (value == null) return;
                            var sourceProperty = typeof(TDestination).GetProperty(map.DestinationField);
                            if (sourceProperty == null) throw new MissingFieldException(map.DestinationField);
                            sourceProperty.SetValue(destination, value.MapTo(sourceProperty.PropertyType));
                        });
                    }
                    break;
                }
                return mappings;
            });

        }

        
    }
}
